为什么 consul 必须在本地部署 agent

说起 consul的使用,其实大家应该对必须要在本地部署一个 agent client 这种做法比较难以适应,或者有人对这个比较反感,我是比较反感这种做法的后者.

想想以前用 zookeeper 做服务发现的的时候在基础代码里面直接指定 server 的地址,在服务启动的时候去注册就好了.现在代码是省了,但是部署的时候需要这种绕着弯子注册的方式挺恶心的,不过恶心归恶心,还是需要自己研究一下为什么要用这种方式.

传说

这种在本地部署 agent 有N种好处,比如:

  1. 分散 consul server的DNS服务发现功能的压力

    扯淡的说法

  2. client 可以缓存所有的service 的地址信息,响应更及时

    这个有可能,我还没看这一块的源代码,无法证实

  3. 由于基于 gessip 协议,所以节点越多,越快达到所有节点信息同步的效果

    可以达到这个效果,但我觉得绝逼不是这个考虑,也不一定client 端就真的是这么实现的,consul 官网上是说 consul 运行时有两套 gessip pool 在工作的,我猜想应该是 server 之间同步用一套 gessip pool, 然后 client 同步用另外一套 gessip pool, 以后看了代码再来补充

  4. 我也想不起来了.

我看到的实际情况

我的环境

>

我在我的树莓派上安装了 consul 的 server,boostrap 模式, ip 为:xxx.xxx.xxx.38

然后在我的笔记本上安装了 client,ip 为:xxx.xxx.xxx.39

写了一个微服务,叫 userservice, 但是我在代码里面直接指定注册到server 上去

userservice 运行在笔记本39这个 Ip上,这个时候在 ui 上可以看到 userservice 是注册到xxx.xxx.xxx.38上,但是注册地址是xxx.xxx.xxx.39:8888

然后我在运行别的微服务的时候,老是提示说请求 userservice 失败,拒绝连接,于是我就用 dig 去查服务对应的 DNS 的 A 记录(具体如何使用请参考DNS INTERFACE)

查到 userservice.service.dc.test. 的解析地址是xxx.xxx.xxx.39 这个是对的,我的服务确实是在38这个 Ip 上,于是我看我的注册代码

摘自register.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
func (this *ConsulServiceRegister) RegService(serviceInfo base.ServiceInfo, endpints []base.EndPoint, servicePort int) base.Error {
ip := base.GetLocalIp()
this.serviceId = fmt.Sprintf("%s-%s", serviceInfo.GetServiceName(), ip)
this.checkId = fmt.Sprintf("service:%s", this.serviceId)
registration := &api.AgentServiceRegistration{
Name: serviceInfo.GetServiceName(),
Tags: base.WarpTags(serviceInfo.GetServiceTags()),
Port: servicePort,
Address: ip.String(),
EnableTagOverride: true,
Checks: api.AgentServiceChecks([]*api.AgentServiceCheck{
{
HTTP: fmt.Sprintf("http://%s:%d/debug/pprof/threadcreate?debug=1", ip, servicePort),
Interval: "10s",
},
{
HTTP: fmt.Sprintf("http://%s:%d/debug/pprof/block?debug=1", ip, servicePort),
Interval: "10s",
},
}),
}
err := this.client.Agent().ServiceRegister(registration)
if err != nil {
logger.Error("注册服务失败:%s", err)
return base.NewError(base.ERROR_CODE_BASE_SERVICE_REGISTER_ERROR,err.Error())
}
return nil
}

我这里在注册时候默认提交了本机的地址,所以在 DNS 查询的时候 A 记录显示本机38这个 Ip 也是对的(PS:好像 java 里面spring集成 consul 的包里面就不会上报本机 Ip)

既然这里的注册信息没有错,然后 DNS 指向的地址也是对的,那没道理访问不到的,我直接 telnet 端口也是通的,于是我把目光集中到 loadbalance 上,这里应该是取到所有 userservice 的可用地址,还有这个地址上对应的端口,但是 A 记录只能取到 IP, 并不能标记端口啊,再看源代码

摘自loadbalancer.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
func newLoadBalancer(nameServer string, port string, lbType clb.LoadBalancerType) *LoadBalancer {
loadBalancer := clb.NewTtlCacheClb(nameServer, port, lbType, 1)
return _newLoadBalancer(loadBalancer)
}
func (this *LoadBalancer) Dial(network, address string) (conn net.Conn, err error) {
host, _, err := net.SplitHostPort(address)
if err != nil {
return nil, err
}
addr, err := this.loadBalancer.GetAddress(host)
if err != nil {
logger.Error("%s\n", err)
return nil, err
}
return this.dialer.Dial(network, addr.String())
}

看11行,这里才是真正获取 Ip 地址和端口的地方,然后继续看代码,这里的this.loadBalancer有两个实现,但是其 Getaddress 的方式是一样,我摘录过来random.go

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
func (lb *RandomClb) GetAddress(name string) (dns.Address, error) {
add := dns.Address{}
srvs, err := lb.dnsLib.LookupSRV(name)
if err != nil {
return add, err
}
if len(srvs) == 0 {
return add, fmt.Errorf("no SRV records found")
}
// log.Printf("%+v", srvs)
srv := srvs[rand.Intn(len(srvs))]
ip, err := lb.dnsLib.LookupA(srv.Target)
if err != nil {
return add, err
}
return dns.Address{Address: ip, Port: srv.Port}, nil
}

分析这段代码,收先从 DNS 上获取了指定地址的 SRV 记录,记录下该 host[userservice.service.dc.test]的端口,然后找到 Target 的 A 记录当做这个 host 的 IP, 这里有一个错了肯定就访问不到了,赶紧的 dig 从本机 Agent Client 上获取 DNS 记录看看.

好吧,我承认这里的 SRV 中的target实际指向了注册节点的域名(consul 的域名规则可以参考上面提到的[DNS INTERFACE]),所以说明 consul 的 DNS 规则是不管你service 注册的时候无论你怎么提交自己的 IP, 在 DNS 上,永远以注册的Consul节点的 IP 作为 Service 的实际 IP,都是 SRV 规则导致的,

所以总结一下,在本地部署 consul Client 的原因是标示 Service 的实际 IP, 这确实也是逼不得已的方式,如果使用 HTTPDNS 的但是,效率不但低,而且算是一个私有协议,不利于和基础环境的融合.

既然搞明白了为什么,那么在什么地方使用效率最高呢,那做个广告,在 Docker 里面其实是最好的,本身 Docker 可以对每个容器分配单独的 IP, 不过如果我们能自动化的把主机端口映射到对应的容器端口,然后注册的时候上报那个外网映射的端口,就可以在一个主机上开 N 个应用,然后共享同一个 client.也许这也是因为使用 SRV 方式必须部署本地的方式的唯一好处.

坚持原创技术分享,您的支持将鼓励我继续创作!